﻿using System;
using System.Reflection;

namespace TypeBuilderFx.Core.Emitters
{
    /// <summary>Emits a class adapting an object (the adaptee) into an interface.</summary>
    public class AdapterEmitter : TypeEmitterBase<AdapterOptions>
    {
        /// <summary>Protected constructor.</summary>
        /// <param name="options"></param>
        protected AdapterEmitter(AdapterOptions options)
            : base(options)
        {
        }

        #region Factory Methods
        /// <summary>Creates a factory of instance of adapter.</summary>
        /// <typeparam name="INTER">Interface type of the adapter.</typeparam>
        /// <typeparam name="ADAPTEE">Type of the adaptee.</typeparam>
        /// <returns>A factory of instance of adapter.</returns>
        public static IFactory<INTER, ADAPTEE> GetFactory<INTER, ADAPTEE>()
            where ADAPTEE : class
            where INTER : class
        {
            return GetFactory<INTER, ADAPTEE, object>();
        }

        /// <summary>Creates a factory of instance of adapter with a given base class.</summary>
        /// <typeparam name="INTER">Interface type of the adapter.</typeparam>
        /// <typeparam name="ADAPTEE">Type of the adaptee.</typeparam>
        /// <typeparam name="BASE">Type of the base class.</typeparam>
        /// <returns>A factory of instance of adapter.</returns>
        public static IFactory<INTER, ADAPTEE> GetFactory<INTER, ADAPTEE, BASE>()
            where ADAPTEE : class
            where INTER : class
            where BASE : class
        {
            AdapterOptions options = new AdapterOptions
            {
                ExposedInterfaceType = typeof(INTER),
                AdapteeType = typeof(ADAPTEE),
                BaseType = typeof(BASE)
            };
            object untypedFactory = GetFactory(options);
            IFactory<INTER, ADAPTEE> factory = untypedFactory as IFactory<INTER, ADAPTEE>;

            if (factory == null)
            {
                throw new ApplicationException("Unable to cast the factory to the proper factory type");
            }

            return factory;
        }

        /// <summary>
        /// Creates a factory of instance of adapter with a given base class taking a parameter in construction.
        /// </summary>
        /// <typeparam name="INTER">Interface type of the adapter.</typeparam>
        /// <typeparam name="ADAPTEE">Type of the adaptee.</typeparam>
        /// <typeparam name="BASE">Type of the base class.</typeparam>
        /// <typeparam name="BASE_CTOR_PARAM">Type of the base class constructor parameter.</typeparam>
        /// <returns>A factory of instance of adapter.</returns>
        public static IFactory<INTER, ADAPTEE, BASE_CTOR_PARAM> GetFactory<INTER, ADAPTEE, BASE, BASE_CTOR_PARAM>()
            where ADAPTEE : class
            where INTER : class
            where BASE : class
        {
            AdapterOptions options = new AdapterOptions
            {
                ExposedInterfaceType = typeof(INTER),
                AdapteeType = typeof(ADAPTEE),
                BaseType = typeof(BASE),
                BaseConstructorParameterTypeList = new Type[] { typeof(BASE_CTOR_PARAM) }
            };
            object untypedFactory = GetFactory(options);
            IFactory<INTER, ADAPTEE, BASE_CTOR_PARAM> factory = untypedFactory as IFactory<INTER, ADAPTEE, BASE_CTOR_PARAM>;

            if (factory == null)
            {
                throw new ApplicationException("Unable to cast the factory to the proper factory type");
            }

            return factory;
        }

        /// <summary>Creates a factory of instance of adapter given options.</summary>
        /// <param name="options"></param>
        /// <returns></returns>
        public static object GetFactory(AdapterOptions options)
        {
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            AdapterEmitter emitter = new AdapterEmitter(options);
            object factory = options.Cache.GetFactory(emitter);

            return factory;
        }
        #endregion

        /// <summary>Emits a factory corresponding to the state of this emitter.</summary>
        /// <param name="componentTypeName"></param>
        /// <param name="factoryTypeName"></param>
        /// <param name="moduleBuilder"></param>
        /// <returns></returns>
        public override object EmitFactory(
            string componentTypeName,
            string factoryTypeName,
            ModuleBuilderExtender moduleBuilder)
        {
            Type adapterInterfaceType = GetAdapterInterfaceType(Options.AdapteeType);
            TypeBuilderExtender typeBuilder = moduleBuilder.CreateType(
                componentTypeName,
                Options.BaseType,
                adapterInterfaceType,
                Options.ExposedInterfaceType);
            Type[] baseClassInterfaces = Options.IsUsingBaseClassInterfaces
                ? TypeHelper.GetAllInterfaces(Options.BaseType, true)
                : new Type[0];

            ImplementAdapterInterface(typeBuilder, Options.AdapteeType);
            DefineAdapterConstructor(
                typeBuilder, Options.AdapteeType, Options.BaseType, Options.BaseConstructorParameterTypeList);

            ImplementInterface(
                typeBuilder,
                Options.ExposedInterfaceType,
                EmitGetMethod,
                EmitSetMethod,
                EmitAddMethod,
                EmitRemoveMethod,
                EmitMethod,
                TypeHelper.Concat(baseClassInterfaces, adapterInterfaceType));

            Type componentType = typeBuilder.TypeBuilder.CreateType();
            object factory =
                CreateFactoryInstance(factoryTypeName, componentType, Options.ExposedInterfaceType, moduleBuilder);

            return factory;
        }

        private void EmitGetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
        {
            PropertyInfo adapteeProperty = Options.AdapteeType.GetProperty(property.Name);

            if (adapteeProperty == null)
            {
                throw new ApplicationException(string.Format("Property '{0}' isn't exposed on the adaptee", property.Name));
            }
            if (adapteeProperty.PropertyType != property.PropertyType)
            {
                throw new ApplicationException(
                    string.Format("Property '{0}' isn't of the same type from the adapter and adaptee.", property.Name));
            }
            if (!adapteeProperty.CanRead)
            {
                throw new ApplicationException(string.Format("Property '{0}' doesn't have a getter on the adaptee", property.Name));
            }

            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            ilGenerator.CallMethod(adapteeProperty.GetGetMethod());
            ilGenerator.ReturnStack();
        }

        private void EmitSetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
        {
            PropertyInfo adapteeProperty = Options.AdapteeType.GetProperty(property.Name);

            if (adapteeProperty == null)
            {
                throw new ApplicationException(string.Format("Property '{0}' isn't exposed on the adaptee", property.Name));
            }
            if (adapteeProperty.PropertyType != property.PropertyType)
            {
                throw new ApplicationException(
                    string.Format("Property '{0}' isn't of the same type from the adapter and adaptee.", property.Name));
            }
            if (!adapteeProperty.CanWrite)
            {
                throw new ApplicationException(string.Format("Property '{0}' doesn't have a setter on the adaptee", property.Name));
            }

            ilGenerator.StackMethodParameter(1);
            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            ilGenerator.CallMethod(adapteeProperty.GetSetMethod());
        }

        private void EmitAddMethod(ILGeneratorExtender ilGenerator, MethodInfo method, EventInfo eventInfo)
        {
            EventInfo adapteeEventInfo = Options.AdapteeType.GetEvent(eventInfo.Name);

            if (adapteeEventInfo == null)
            {
                throw new ApplicationException(string.Format("Event '{0}' isn't exposed on the adaptee", eventInfo.Name));
            }
            if (adapteeEventInfo.EventHandlerType != eventInfo.EventHandlerType)
            {
                throw new ApplicationException(
                    string.Format("Event '{0}' isn't of the same type from the adapter and adaptee.", eventInfo.Name));
            }

            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            ilGenerator.StackMethodParameter(1);
            ilGenerator.CallMethod(adapteeEventInfo.GetAddMethod());
            ilGenerator.ReturnStack();
        }

        private void EmitRemoveMethod(ILGeneratorExtender ilGenerator, MethodInfo method, EventInfo eventInfo)
        {
            EventInfo adapteeEventInfo = Options.AdapteeType.GetEvent(eventInfo.Name);

            if (adapteeEventInfo == null)
            {
                throw new ApplicationException(string.Format("Event '{0}' isn't exposed on the adaptee", eventInfo.Name));
            }
            if (adapteeEventInfo.EventHandlerType != eventInfo.EventHandlerType)
            {
                throw new ApplicationException(
                    string.Format("Event '{0}' isn't of the same type from the adapter and adaptee.", eventInfo.Name));
            }

            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            ilGenerator.StackMethodParameter(1);
            ilGenerator.CallMethod(adapteeEventInfo.GetRemoveMethod());
            ilGenerator.ReturnStack();
        }

        private void EmitMethod(ILGeneratorExtender ilGenerator, MethodInfo method)
        {
            MethodInfo adapteeMethod = Options.AdapteeType.GetMethod(method.Name);

            if (adapteeMethod == null)
            {
                throw new ApplicationException(string.Format("Method '{0}' isn't exposed on the adaptee", method.Name));
            }
            if (adapteeMethod.ReturnType != method.ReturnType)
            {
                throw new ApplicationException(string.Format(
                    "Method '{0}' return type isn't of the same type from the adapter and adaptee.",
                    method.Name));
            }
            if (adapteeMethod.GetParameters().Length != method.GetParameters().Length)
            {
                throw new ApplicationException(string.Format(
                    "Method '{0}' doesn't have the same number of parameters from the adapter and adaptee.",
                    method.Name));
            }
            for (int i = 0; i != adapteeMethod.GetParameters().Length; ++i)
            {
                if (adapteeMethod.GetParameters()[i].ParameterType != method.GetParameters()[i].ParameterType)
                {
                    throw new ApplicationException(string.Format(
                        "Parameter {0} on method '{1}' doesn't have the same type from the adapter and adaptee.",
                        method.GetParameters()[i].Name,
                        method.Name));
                }
                if (adapteeMethod.GetParameters()[i].IsOut != method.GetParameters()[i].IsOut
                    || adapteeMethod.GetParameters()[i].IsIn != method.GetParameters()[i].IsIn)
                {
                    throw new ApplicationException(string.Format(
                        "Parameter {0} on method '{1}' doesn't have the same attribute in/out from the adapter and adaptee.",
                        method.GetParameters()[i].Name,
                        method.Name));
                }
            }
            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            for (int i = 0; i != adapteeMethod.GetParameters().Length; ++i)
            {
                ilGenerator.StackMethodParameter((short)(i + 1));
            }
            ilGenerator.CallMethod(adapteeMethod);
            ilGenerator.ReturnStack();
        }
    }
}